Wi-Fi Android
Internet of Things
Wi-Fi Android
Camellia, 2020.2.15
Courtesy of Gleb Tagirov
无线网络( Wireless )技术被广泛地使用在智能机器、健康监测、共享单车、智能家电等设备的通讯中,使智能设备轻松地与手机、平板和电脑等交换数据信息。1895年,意大利无线电工程师、企业家伽利尔摩·马可尼( Guglielmo Marconi ),在意大利的博洛尼亚( Bologna )成功地把无线电信号发送到了1.5英里外的地方,他成为了世界上第一台实用的无线电报系统的发明者,并于1909年获得诺贝尔物理学奖( In recognition of his contributions to the development of wireless telegraphy ),被称作“无线电之父”。
随着广播和电台的不断增加,无线电通讯之间相互干扰的现象越来越严重,因此政府建立了频谱授权制度,对无线电通讯进行统一管理。20世纪80年代,微电路和数字信号处理等技术迅速发展,新发明的无线设备不断涌现,但受限于频谱限制,新发明无法被灵活和推广使用。
此时,美国联邦通信委员会( Federal Communications Commission )的工程师迈克尔·马库斯( Michael Marcus )建议:可以提供一些未授权的频谱,便于行业使用,并适当增加这些未授权频谱设备的发射功率,使之可以覆盖几十到几百米的范围。这样将有利于激励科技企业的创新,带来更多的经济效益。美国联邦通信委员会采纳了他的建议,并向社会各界征求意见,并最终释放出三个不受欢迎的“垃圾频段”,即 Industrial Scientific Medical Band 频段,这些频段主要开放给工业、科学、医学三个领域使用,属于免费授权( Free License ),并且设备发射功率可达到1W。也正是这1W,成就了今天的 Wi-Fi,蓝牙 Bluetooth, ZigBee 等各种短距离通信技术。迈克尔·马库斯( Michael Marcus )也因此,被称为" Wi-Fi 的教父",曾被英国《经济学人 The Economist 》誉为最有远见的工程师。
The seeds Marcus sowed there would go on to grow thousands of thousands of wireless technologies worth untold billions of dollars.为了避免设备间干扰,美国联邦通信委员会还要求这些免授权频段的产品使用扩频技术。但是整个产业无线通讯产品的设备商们,仍是各自开发自己的专用设备,行业没有统一的标准。
扩频通信是扩展频谱通信( Spread Spectrum Communication )的简称,就是传输信息所用的带宽远大于信息本身带宽,在发射端以扩频编码进行扩频调制,在接收端以相关解调技术接收信息。扩频技术最早应用于军事领域,具备高可靠性,高保密性且不易受到干扰等特性。
Intersil 、 3Com 、 Nokia 、 Aironet 、 Symbol 和 Alcatel-Lucent 6家公司,共同组成了无线以太网路相容性联盟( Wireless Ethernet Compatibility Alliance ),对不同厂家的产品进行兼容性认证,实现不同厂家设备间的互操作性。联盟成立之后,为了便于市场推广,于2002年10月选取 Wi-Fi 作为其名称, Wi-Fi 有着 wireless fidelity(无线保真)的含义。WECA 改名为 Wi-Fi 联盟( Wi-Fi Alliance )。
我国自主研发、拥有自主知识产权的无线局域网安全技术标准 Wireless LAN Authentication and Privacy Infrastructure(无线局域网鉴别与保密基础结构)与 Wi-Fi 是两种不同的协议。出于对互联网安全的考虑,我国一直强烈建议推荐 WAPI 作为一个独立的国际标准。2010年,在国际标准化组织( International Organization for Standardization )和国际电工委员会( International Electrotechnical Commission )第一联合技术委员会( JTC1 )第六分技术委员会( SC6 )全会会议上,WAPI 首次获得美、英、法等10余个国家成员的一致同意,将以独立文本形式推进为国际标准。
Wi-Fi 构建无线局域网络的工作模式
建立上述物理层的连接后,采用 Transmission Control Protocol 的通讯方式,并把智能设备作为服务器,手机、平板、电脑等作为客户端,客户端通过 Internet Protocol 地址和端口号连接服务器。
字符编码
计算机本质是识别0和1构成的信息,如何把0和1转换为人类能够识别的数字、字母、文字和符号呢?需要采用不同的编码,即每个数字、字符、文字和符号等都对应一个数值。当然每个人都可以约定自己的一套编码规则,而大家如果要想互相通信而不造成混乱,那么大家就必须遵循相同的编码规则。
Courtesy of jamie oliver aspinall
American Standard Code for Information Interchange 由 American National Standard Institute 制定的,是一种标准的单字节字符编码方案,后来被 International Organization for Standardization 定为国际标准 ISO 646:1983 Information processing - ISO 7-bit coded character set for information interchange ,适用于所有拉丁文字字母。
ASCII 码使用指定的7位或8位二进制数组合来表示128 或256 种可能的字符。标准ASCII 码使用7位二进制数来表示所有的大写和小写字母、数字0到9、标点符号,以及在美式英语中使用的特殊控制字符,其最高位用作奇偶校验位。
扩展ASCII 码将最高位用于确定附加的128 个特殊符号字符、外来语字母和图形符号 。
字符 | ASCII (BIN) |
---|---|
C | 0100 0011 |
a | 0110 0001 |
m | 0110 1101 |
e | 0110 0101 |
l | 0110 1100 |
l | 0110 1100 |
i | 0110 1001 |
a | 0110 0001 |
但是世界上各国的语言远不止拉丁文字字母。因此又诞生了 Unicode ,其把所有语言都统一到一套编码里,为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
这里先提及另一种编码规则,Universal Coded Character Set 是 International Organization for Standardization 制定的 ISO/IEC-10646 标准所定义的标准字符集,包括 UTS-2 和 UTS-4 。其中 UCS-4 中采用4个字节31位,即 0 至 0x7FFFFFFF 进行编码:
- 最高字节:该字节的最高位为0,其作用是形成128个组(group)。
- 次高字节:把每个组(group)再分为256个平面(plane)。
- 第3个字节:把每个平面再分为256行(row)。
- 第4个字节:把每行分为256个码位(cell)。
以上是 Unicode 对数字、字母、文字和符号等的编码方式,实际中,为了节约存储空间,可以采用不同的方式将 Unicode 编码表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF 代表 Unicode Transformation Format,可理解为 Unicode 字符集的转换存储格式。
UTF-8 以字节为单位对 Unicode 进行转换存储,其特点是对不同范围的字符使用不同长度的编码存储方式。UTF-8 编码的最大长度是6个字节,实际上 Unicode 的最大码位 0x10FFFF 只有21位,因此 UTF-8 编码实际最多为4个字节。
- 如果只有一个字节则其最高二进制位为0,例如对于 0x00-0x7F 之间的字符,UTF-8 编码与 ASCII 编码完全相同,仅采用1个字节。
- 如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头。
字节数 | 编码位数 | 编码(HEX) | 第1字节 (最高) |
第2字节 (次高) |
第3字节 (次低) |
第4字节 (最低) |
---|---|---|---|---|---|---|
1 | 7 | 0000 007F | 0xxx xxxx | - | - | - |
2 | 11 | 0080 07FF | 11xx xxxx | 10xx xxxx | - | - |
3 | 16 | 0800 FF7F | 111x xxxx | 10xx xxxx | 10xx xxxx | - |
4 | 21 | 10000 10FFFF | 1111 xxxx | 10xx xxxx | 10xx xxxx | 10xx xxxx |
上表中, UTF-8 转换 Unicode 编码时,将 Unicode 编码的二进制数从低位往高位每次取6位,填入各字节(从低到高)。
UTF-16 采用16位无符号整数转换 Unicode 编码,以U表示 Unicode 编码:
- 当 U 小于 0x10000,其 UTF-16 的编码存储方式就是 Unicode 对应的16位无符号整数。
- 当 U 大于等于 0x10000,其 UTF-16 的编码存储方式为一对16位的码元(即32位,4个字节),这称作代理对(Surrogate Pair),先计算 U' = U - 0x10000 ,然后把计算结果U'写成20位的二进制形式 yyyy yyyy yyxx xxxx xxxx,整个 Unicode 编码的 UTF-16 编码存储方式(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx,其中1101100(0xD800)称作前导代理( lead surrogate )、1101110(0xDC00)称作后尾代理( trail surrogate )。
U' = yyyyyyyyyyxxxxxxxxxx // U - 0x10000
W1 = 110110yyyyyyyyyy // 0xD800 + yyyyyyyyyy
W2 = 110111xxxxxxxxxx // 0xDC00 + xxxxxxxxxx
UTF-32 采用32位无符号整数转换 Unicode 编码。Unicode 的 UTF-32 转换编码就是其对应的32位无符号整数,没有变化。因为UTF-32对每个字符都使用4个字节,其存储空间、数据传输长度等的效率较低。
GBK 汉字内码扩展规范:GBK 取自“国标”、“扩展”的汉语拼音的第一个字母,英文名称为Chinese Internal Code Specification 。其是在 GB2312 标准基础上的内码扩展规范,使用了双字节编码方案,总体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F,总计 23940 个码位,共收入 21886 个汉字和图形符号,其中汉字(包括部首和构件)21003 个,图形符号883个,完全兼容 GB2312 标准,支持 ISO/IEC10646-1 和 GB13000-1 中的全部中、日、韩字符,并包含了 BIG5 编码中的所有汉字。
字符 | GBK (BIN) | Unicode (BIN) | UTF-8 (BIN) |
---|---|---|---|
欣 | 1101 0000 1100 0000 |
0110 1011 0010 0011 |
1110 0110 1010 1100 1010 0011 |
欣 | 1101 0000 1100 0000 |
0110 1011 0010 0011 |
1110 0110 1010 1100 1010 0011 |
人 | 1100 1000
1100 1011 |
0100 1110
1011 1010 |
1110 0110
1011 1010 1011 1010 |
类 | 1100 0000
1110 0000 |
0111 1100
0111 1011 |
1110 0111
1011 0001 1011 1011 |
的 | 1011 0101
1100 0100 |
0111 0110
1000 0100 |
1110 0111
1001 1010 1000 0100 |
境 | 1011 1110
1011 0011 |
0101 1000
1000 0011 |
1110 0101
10100010 1000 0011 |
界 | 1011 1101
1110 0111 |
0111 0101
0100 1100 |
1110 0111
1001 0101 1000 1100 |
GBK 与 UTF-8 、 UTF-16 之间都必须通过 Unicode 编码才能相互转换。
Internet Protocol 地址
Courtesy of Randy Glasbergen
物联网时代的到来,每台设备都需要具有一个唯一标识的地址用于在网络的互联中进行相互通讯。
我们发信、打电话和邮寄快递都需要知道对方的地址或电话号码,还需要同时记录自己的电话号码或地址信息。Internet Protocol 的任务就是把数据从源设备传送到目的设备,而源设备和目的设备都需要自己的地址。Internet Protocol 根据数据包中报头中包括的目的地址将数据报传送到目的设备,在此过程中, Internet Protocol 还负责选择传送的道路,这称为路由功能。
IPv4 最早于1983年用于APPANET Advanced Research Project Agency, 其为美国国防部高级研究计划署开发的世界上第一个运营的封包交换网络,它是全球互联网的始祖。
IPv4 地址是有限性,虽然可以通过路由器和交互机建立局域网,把一个在广域网的节点拓展为多个内部节点,但是物联网时代的到来:一方面每台智能设备都需要一个地址,而不仅仅限于电脑,因此地址数量的需求会极大幅度地增加;一方面每台智能设备在全球如果地址唯一,并能够直接互联,将实现终端到终端(end-to-end)的全球网络通讯,全球每台设备可以直接对另一台设备进行寻址通讯,大大减少路由分析,提高通讯的速度。
IPv6 采用128位编码,通常采用8组4位16进制数进行标识,每组之间采用冒号分隔。理论上可以标识3.4x10^38个网络地址,因此可以为更多的设备分配地址,还可以分级进行地址分配,便利路由聚合(route aggregation)、简化多路广播(multicast)。
One undercillion means 1 x 10^34
Courtesy of TraceMyIP.org
物联网时代,IPv6 将可以为每一粒沙子分配一个网络地址。Android 客户端
Courtesy of Santiago Sarquis
下面详细介绍 Android 作为客户端的软件开发流程。采用 Socket,并基于 Transmission Control Protocol 协议发送和接收数据信息。
Socket 即套接字,是应用层与 TCP/IP 协议族通信的中间软件抽象层,表现为一个封装了 TCP/IP 协议族的编程接口( Application Programming Interface ),一个 Socket 实例代表一个主机上的一个应用程序的通信链路。
Transmission Control Protocol 传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议。为了保证传输的可靠性,TCP 协议采用三次对话的确认机制,保证在正式收发数据前,建立可靠的连接:
第一次握手:建立连接时,客户端发送SYN包(SYN=j)到服务器,进入 SYN_SEND 状态,并等待服务器确认。
第二次握手:服务器收到SYN包,必须确认客户的SYN(ACK=j+1),同时自己也发送另一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入 SYN_RECV 状态。
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态。
通讯中,每发出一个数据包都要求对方确认,如果有一个数据包丢失,发送方就收不到对方的确认,就必须重发这个数据包。
硬件检查
在手机设置中,连接路由器,请参见上文,根据不同的工作模式,连接所需要的路由器。
程序中首先检查手机的 Wi-Fi 功能是否开启,并通过检查手机 IP 地址等方式检测是否与路由器相连通。
MainActivity activity = (MainActivity) getActivity();
Context mcontext = activity.getApplicationContext();
ConnectivityManager mConnectivityManager = (ConnectivityManager) mcontext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if( mWiFiNetworkInfo != null && mWiFiNetworkInfo.isConnected() ) {
WifiManager mWifiManager = (WifiManager) mcontext.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
// ...
}
else{
// ...
}
定义变量
定义 Socket 变量,并分别定义 OutputStream 和 InputStream 作为发送和接收的操作变量,同时定义服务器的 IP 地址和端口号变量。不同的应用程序采用不同的端口。
private Socket mSocket = new Socket();
private OutputStream mOutputStream = null;
private InputStream mInputStream = null;
private InetAddress mIPAddress;
public int mDevicePortWiFi;
public String mDeviceAddressWiFi = "192.168.4.1"; // 服务器的 IP
private byte[] bufWiFi = new byte[64];
定义线程设置,这里对于连接操作、发送数据操作、接收数据操作分别采用不同的子线程。
private ConnectSeverThread mConnectThread;
private SendtoSeverThread mSendThread;
private ReceivefromSeverThread mReceiveThread;
连接服务器
调用连接操作子线程。实际编程中,可以设置一些标志变量,例如是否已经连接,避免重复连接。
public void ConnecttoServer(){
mConnectThread = new ConnectSeverThread();
mConnectThread.start();
}
在子线程中,需要首先对 Socket 变量进行初始化,确定需要连接的 TCP 服务器的 IP 地址和应用程序的端口号。
private class ConnectSeverThread extends Thread {
public void run() {
try
{
mIPAddress = InetAddress.getByName(mDeviceAddressWiFi);
mSocket = new Socket(mIPAddress, mDevicePortWiFi);
// 或者采用下面的方法,可以设置连接的超时时间
// mSocket = new Socket();
// SocketAddress socAddress = new InetSocketAddress(mIPAddress, mDevicePortWiFi);
// mSocket.connect(socAddress, 5000);
runOnUiThread(new Runnable()
{
public void run() {
}
});
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
发送数据
当需要发送数据时,调用下面的函数:
public void sendDataWiFi(byte[] data) {
if( mSocket.isClosed() == false ) {
for( i = 0 ; i < 64 ; i++ ){
bufWiFi[i] = data[i];
}
mSendThread = new SendtoSeverThread();
mSendThread.start();
}
}
触发接收数据线程:
private class SendtoSeverThread extends Thread {
public void run() {
if( mSocket.isClosed() == false ) {
try
{
mOutputStream = mSocket.getOutputStream();
mOutputStream.write(bufWiFi);
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
}
接收数据
当需要接收数据时,调用下面的函数:
public void receiveDataWiFi( ) {
if( mSocket.isClosed() == false ) {
mReceiveThread = new ReceivefromSeverThread();
mReceiveThread.start();
}
}
触发接收数据线程:
private class ReceivefromSeverThread extends Thread {
public void run() {
if( mSocket.isClosed() == false ) {
try
{
mInputStream = mSocket.getInputStream();
// 中文字符,采用 GB18030 编码
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(mInputStream,"GB18030"));
// 不考虑中文字符,可以采用 UTF-8 编码
// BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(mInputStream,"UTF-8"));
}
catch(UnsupportedEncodingException e)
{
e.printStackTrace();
}
}
}
}
Android 接收数据时,是以"\n"作为每次接收结束的标志,因此其他设备发送数据时,请在数据信息结尾增加"\n",以防止 Android 接收端无休止地等待。
线程消息传递
Android 中不允许子线程操作主界面,因此如何把主界面的数据传递给发送数据子线程,或是如何把接收数据子线程中的数据显示到主界面上,就需要采用线程中的消息传递方法。
在子线程中传递消息:
Message msg = new Message();
handler.sendMessage(msg);
在主线程中处理子线程发送来的消息:
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
心跳程序
Android 的 Socket 通讯属于长连接,客户端侧不需要手动关闭。
有时考虑到功耗等问题,服务器端会设置休眠等功能,长时间接收不到客户端的信息时,会自动断开连接,此时在客户端侧可以建立心跳(Heart Beat)函数,定期向服务器发送简短的、无关实际操作的数据信息。
每个安卓版本就是一道美味零食
Verison | Sweet Name | 美味零食 |
---|---|---|
10 | Q | ??? |
9 | Pie | 馅饼 |
8 | Oreo | 奥利奥饼干 |
7 | Nougat | 牛乳糖 |
6 | Marshmallow | 棉花糖 |
5 | Lollipop | 棒棒糖 |
4.4 | Kitkat | 奇巧巧克力 |
4.3 | Jelly Bean | 软心豆粒糖 |
4.1 | Jelly Bean | 软心豆粒糖 |
4.1 | Jelly Bean | 软心豆粒糖 |
4.0 | Ice Cream Sandwich | 冰淇凌三明治 |
3 | Honeycomb | 蜂巢 |
3 | Marshmallow | 棉花糖 |
2.3 | Gingerbread | 姜饼 |
2.2 | Froyo | 冻酸奶 |
2.1 | Eclair | 巧克力长形奶油松饼 |
2.0 | Eclair | 巧克力长形奶油松饼 |
1.6 | Doughnut | 炸面圈 |
1.5 | Cupcake | 杯形蛋糕 |
1.1 | Banana Bread | 香蕉面包 |
1.0 | Apple Pie | 苹果馅饼 |
Courtesy of Mauro Martins